既然知道了 EventMachine 在 Unlight 專案中扮演了處理 TCP 連線的角色,不過實際上又是怎麼設計跟實作的呢?
我們先來看一下在 EventMachine 在 GitHub 提供的 EchoServer 範例
require 'eventmachine'
module EchoServer
def post_init
puts "-- someone connected to the echo server!"
end
def receive_data data
send_data ">>>you sent: #{data}"
close_connection if data =~ /quit/i
end
def unbind
puts "-- someone disconnected from the echo server!"
end
end
# Note that this will block current thread.
EventMachine.run {
EventMachine.start_server "127.0.0.1", 8081, EchoServer
}
看起來跟我們原本使用的原生 Ruby 版本似乎更容易使用多了,在 EventMachine 可以透過定義一個模組或者物件用來處理連線,只需要有對應的方法即可(像是 #receive_data
和 #unbind
等等)
直接搜尋 Unlight 的原始碼看看,因此會發現有好幾個檔案都使用了 EventMachine.start_server
[elct9620] server ‹master› % ag 'start_server'
src/raid_chat.rb
40: EM.start_server "0.0.0.0", SV_PORT, RaidChatServer
src/data_lobby.rb
39: EM.start_server "0.0.0.0", SV_PORT, DataServer
src/watch.rb
45: EM.start_server "0.0.0.0", SV_PORT, WatchServer
src/raid.rb
32: EM.start_server "0.0.0.0", SV_PORT, RaidServer
src/raid_data.rb
41: EM.start_server "0.0.0.0", SV_PORT, RaidDataServer
src/quest.rb
32: EM.start_server "0.0.0.0", SV_PORT, QuestServer
src/lobby.rb
41: EM.start_server "0.0.0.0", SV_PORT, LobbyServer
src/authentication.rb
42: EM.start_server "0.0.0.0", $SV_PORT, AuthServer
src/chat.rb
41: EM.start_server "0.0.0.0", SV_PORT, ChatServer
src/raid_rank.rb
41: EM.start_server "0.0.0.0", SV_PORT, RaidRankServer
src/global_chat.rb
40: EM.start_server "0.0.0.0", SV_PORT, GlobalChatServer
src/game.rb
45: EM.start_server "0.0.0.0", SV_PORT, GameServer
src/matching.rb
48: EM.start_server "0.0.0.0", SV_PORT, MatchServer
在 Ruby 裡面因為沒有 Interface (介面)的概念,取而代之的是 Duck Typing 的做法,也就是只要有對應的方法就可以被視為該種類型的物件或者介面。因此在 EventMachine 的範例中,只要是某個物件的實例(Instance)有相關的方法就可以被呼叫。而 Ruby 中所有東西都是物件,所以一個 Module 其實是 Module 物件的實例,也就符合了這個條件。
Unlight 會切割出這麼多伺服器的情境來看,以做 Web 服務的角度來看可能會有點疑惑,不過如果我們用最近幾年比較熱門的 MicroService (微服務) 來說明這個情境可能就會相對的有能接受這樣的做法。另一方面在遊戲伺服器的設計中,如果都集中在一個伺服器上處理,那麼某些特別消耗資源的步驟就會拖累其他人。因此拆分開來變成獨立的伺服器就能夠針對某個類型伺服器做拓展,這其實也是我們在線上遊戲常見的「分流」的概念。
如果從架構層面來看,其實就是 SOA 或者 MicroService 的方式去設計,只是切割的單位大或小的差異。雖然我們不可能直接將這樣的設計跟思考直接搬到其他類型的應用上,但是切割服務的方式以及如何在不同服務之間溝通的設計仍是我們值得參考跟學習的地方。
在 Unlight 原始碼中看到
EM
而不是EventMachine
的原因是因為EM
是EventMachine
的別名。
接著我們以 src/authentication.rb
這個檔案為基礎來追蹤,因為登入伺服器通常是遊戲的入口用來當作閱讀的起點是不錯的選項。下面的 AuthServer
位於 src/protocol/authserver.rb
這個位置。
module Unlight
module Protocol
class AuthServer < ULServer
# クラスの初期化
def self.setup
super
Player.auth_off_all
# コマンドクラスをつくる
@@receive_cmd=Command.new(self,:Auth)
# 暗号化クラスを作る
@@srp = SRP.new()
@@invited_id_set = []
end
我們先關注最開始的地方,這個檔案繼承自 ULServer
這個檔案,因此先往回看一下 src/protocol/ulserver.rb
來確認 AuthServer
繼承了哪些特性。
module Unlight
module Protocol
class ULServer < EventMachine::Connection
# これ以上前に反応していなかった切る
CONNECT_LIVE_SEC = 3600 # 1時間
# ...
# データの受信
def receive_data data
a = data2command(data)
@command_list += a unless a.empty?
do_command
end
# 切断時
def unbind
SERVER_LOG.info("#{@@class_name}: [Connection close] #{@ip}")
end
# ...
end
end
前面有提到 EventMachine 提供了不少輔助方法來讓我們可以處理連線,在 ULServer
裡面就定義了文章最開頭所展示的 EventMachine 版本 EchoServer 所需的 #receive_data
和 #unbind
等方法,由此可見透過 EM.start_server
的 AuthServer
是符合 EventMachine 的要求。
因為 EventMachine 已經幫我們解決了管理 TCP 連線的部分,所以我們只需要關注在怎麼「處理」這些連線。每當我們接受一個玩家連線時,EventMachine 就會產生一個 AuthServer
實例(一個玩家對應一個 Server Instance)並且在收到資料的時候呼叫 #receive_data
方法來解析玩家的操作,並進行後續的動作。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。